/*
 * Copyright 2006 George Georgovassilis <g.georgovassilis[at]gmail.com>
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *    http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.javaongems.user.gwthandler;

import java.lang.reflect.Method;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.LoaderClassPath;
import javassist.Modifier;
import javassist.NotFoundException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.gwtwidgets.server.spring.GWTSpringController;
import org.gwtwidgets.server.spring.ReflectionUtils;
import org.gwtwidgets.server.spring.enhancer.ClassEnhancer;
import org.gwtwidgets.server.spring.enhancer.GWTSpringDelegatingController;
import org.springframework.aop.framework.Advised;

import com.google.gwt.user.client.rpc.RemoteService;

/**
 * Class enhancer based on the Javassist instrumentation library.
 * notes: refactored george's impl to support rpcImp#setThreadLocalHttpServletRequest()
 * 
 * @author Ashin Wimalajeewa (ash)
 * 
 */
public class JavassistEnhancer implements ClassEnhancer {
    private final Log logger = LogFactory.getLog(getClass());

	public synchronized GWTSpringController createController(RemoteService delegate, boolean unwrapExceptions) {
		try {
			ControllerBuilder builder = new ControllerBuilder(delegate);
			return builder.build();
		} catch (RuntimeException e) {
			throw e;
		} catch (Exception e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}
	
	static public RemoteService getRemoteServiceBase(RemoteService impl) throws Exception{
		RemoteService base = impl;
		if (impl instanceof Advised) {
			Advised advisedImpl = (Advised) impl;
			base = (RemoteService) advisedImpl.getTargetSource().getTarget();
			base = getRemoteServiceBase(base);
		}
		return base;
	}
	
	protected class ControllerBuilder {
		static final public String DELEGATE_PREFIX = "__gwtHndlrDelegate";
		static final public String BASE_DELEGATE = "__gwtHndlrBase";
		
		private int delegateHashCode; 
		private RemoteService delegate, baseDelegate;
		private Class baseClass;
		private boolean isHttpServletRequestRequired;
		private ClassPool pool;
		private CtClass controllerCtClass, baseCtClass;
		private Class[] baseInterfaces;
		
		public ControllerBuilder(RemoteService impl) throws Exception {
			delegate = impl;
			delegateHashCode = delegate.hashCode();
			baseDelegate = getRemoteServiceBase(delegate);
			baseClass = baseDelegate.getClass();
			baseInterfaces = ReflectionUtils.getImplementedInterfaces(baseClass);
			Method setMth = Utils.querySetThreadLocalHttpServletRequestMethod(baseClass);
			isHttpServletRequestRequired = (setMth != null);
		}
		
		public GWTSpringDelegatingController build() 
				throws NotFoundException, NoSuchMethodException, CannotCompileException, InstantiationException, IllegalAccessException {
			prepareClassPool();
			constructControllerClass();
			appendDelegateFields();
			appendDelegateInitialiser();
			appendRemoteServiceInterfaces();
			return constructController();
		}
		
		private void prepareClassPool() throws NotFoundException {
			pool = ClassPool.getDefault();
			ClassLoader delegateClassLoader = baseClass.getClassLoader();
			pool.insertClassPath(new LoaderClassPath(delegateClassLoader));
			baseCtClass = pool.get(baseClass.getName());
			if (logger.isDebugEnabled())
				logger.debug(pool + " pool initialised");
		}
		
		private void constructControllerClass() throws NoSuchMethodException, NotFoundException, CannotCompileException {
			String superClassName = getControllerSuperClassname();
			String className =  superClassName + delegateHashCode;
			controllerCtClass = pool.makeClass(className);
			controllerCtClass.setSuperclass(pool.get(superClassName));
			if (logger.isDebugEnabled())
				logger.debug(controllerCtClass + " constructed");
		}

		private String getControllerSuperClassname() {
			String superClassName = GWTSpringDelegatingController.class.getName();
			return superClassName;
		}
		
		private void appendRemoteServiceInterfaces() throws NotFoundException, CannotCompileException {
			for (int i = 0; i < baseInterfaces.length; i++) {
				CtClass ctIntf = pool.get(baseInterfaces[i].getName());
				appendRemoteService(ctIntf, i);
			}
			if (logger.isDebugEnabled())
				logger.debug(controllerCtClass + " all interfaces added");
		}
		
		private void appendRemoteService(CtClass ctIntf, int idx) throws CannotCompileException, NotFoundException {
			controllerCtClass.addInterface(ctIntf);
			CtMethod[] intfMthds = ctIntf.getDeclaredMethods();
//			for (CtMethod method : intfMthds) 
//				appendInterfaceMethod(method, idx);
			for (int i = 0; i < intfMthds.length; i++)
				appendInterfaceMethod(intfMthds[i], idx);
			if (logger.isDebugEnabled())
				logger.debug(controllerCtClass + " " + ctIntf);
		}
		
		private void appendInterfaceMethod(CtMethod intfMtd, int idx) throws CannotCompileException, NotFoundException {
			CtMethod toAppend = CtNewMethod.copy(intfMtd, controllerCtClass, null);
			int mods = toAppend.getModifiers();
			mods ^= Modifier.ABSTRACT;
			toAppend.setModifiers(mods);
			String body = constructMethodBody(toAppend, idx);
			toAppend.setBody(body);
			controllerCtClass.addMethod(toAppend);
			if (logger.isDebugEnabled())
				logger.debug(controllerCtClass + " " + toAppend);
		}
		
		private String constructMethodBody(CtMethod intfMtd, int idx) throws NotFoundException {
			CtClass[] params = intfMtd.getParameterTypes();
			boolean retSpec = !CtClass.voidType.equals(intfMtd.getReturnType());
			
			StringBuilder body = new StringBuilder("{\r\n");
			if (isHttpServletRequestRequired) 
				body.append(BASE_DELEGATE).append(".setThreadLocalHttpServletRequest(this.getThreadLocalRequest());\r\n");
			if (retSpec)
				body.append("return ");
			body.append(DELEGATE_PREFIX).append(idx).append('.');
			body.append(intfMtd.getName());
			body.append('(');
			for (int i = 0; i < params.length; i++) {
				if (i > 0)
					body.append(',');
				body.append('$').append(i+1);
			}
			body.append(");\r\n}");
			return body.toString();
		}
		
		private void appendDelegateFields() throws CannotCompileException, NotFoundException {
			for (int i = 0; i < baseInterfaces.length; i++) {
				CtClass intf = pool.get(baseInterfaces[i].getName());
				CtField delegate = new CtField(intf, DELEGATE_PREFIX + i, controllerCtClass);
				delegate.setModifiers(Modifier.PRIVATE);
				controllerCtClass.addField(delegate);
			}
			if (isHttpServletRequestRequired) {
				CtField base = new CtField(baseCtClass, BASE_DELEGATE, controllerCtClass);
				base.setModifiers(Modifier.PRIVATE);
				controllerCtClass.addField(base);
			}
			if (logger.isDebugEnabled())
				logger.debug(controllerCtClass);
		}
		
		private void appendDelegateInitialiser() throws CannotCompileException {
			StringBuilder meth = new StringBuilder();
			// signature
			meth.append("public void __GWTSpringDelegatingController__setDelegate(").append(RemoteService.class.getName()).append(" p1)\r\n");
			
			// body
			meth.append("{\r\n");
			for (int i = 0; i < baseInterfaces.length; i++) 
				meth.append("  ").append(DELEGATE_PREFIX).append(i).append("= (").append(baseInterfaces[i].getName()).append(") p1;\r\n"); 
			if (isHttpServletRequestRequired)
				meth.append("  ").append(BASE_DELEGATE).append(" = (").append(baseClass.getName()).append(") ").append(JavassistEnhancer.class.getName()).append(".getRemoteServiceBase(p1);\r\n");
			meth.append('}');
		
			CtMethod init = CtNewMethod.make(meth.toString(), controllerCtClass);
			controllerCtClass.addMethod(init);
			if (logger.isDebugEnabled())
				logger.debug(controllerCtClass + " " + init);
		}
		
		private GWTSpringDelegatingController constructController() throws CannotCompileException, InstantiationException, IllegalAccessException {
			Class controllerClass = controllerCtClass.toClass();
			GWTSpringDelegatingController ret = (GWTSpringDelegatingController) controllerClass.newInstance();
			ret.__GWTSpringDelegatingController__setDelegate(delegate);
			if (logger.isDebugEnabled())
				logger.debug("ctrl = " + ret);
			return ret;
		}
	}
}